package fr.cephb.locustree.admin;

import java.io.PrintStream;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;


import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.Transaction;

import fr.cephb.io.IOUtils;
import fr.cephb.json.parser.JSONParser;
import fr.cephb.locustree.ChromInfo;
import fr.cephb.locustree.LTModel;
import fr.cephb.locustree.LTNode;
import fr.cephb.locustree.LTSqlLoader;
import fr.cephb.locustree.LTStreamLoader;
import fr.cephb.locustree.MappedObject;
import fr.cephb.locustree.Organism;
import fr.cephb.locustree.Track;
import fr.cephb.util.Cast;
import fr.cephb.util.StringUtils;
import fr.cephb.xml.XML;

/**
 * LTLoadComponents
 * @author pierre
 *
 */
public class LTLoadComponents extends AbstractLocusTreeCmd
	{
	private REPLACE_POLICY replacePolicy=REPLACE_POLICY.ERROR;
	private Boolean transactional=null;
	private LTLoadComponents()
		{
		}
	
	@Override
	protected void usage(PrintStream out)
		{
		super.usage(out);
		out.println("-nt force NOT transactional");
		out.println("-R <replace-policy one of: ["+
			REPLACE_POLICY.ERROR+","+
			REPLACE_POLICY.UPDATE+","+
			REPLACE_POLICY.IGNORE+"] default:"+
			this.replacePolicy
			);
			
		}
	
	@Override
	protected int processArg(String[] args, int optind)
		{
		if(args[optind].equals("-R"))
			{
			this.replacePolicy= REPLACE_POLICY.valueOf(args[++optind].toUpperCase());
			return optind;
			}
		else if(args[optind].equals("-nt"))
			{
			this.transactional= Boolean.FALSE;
			return optind;
			}
		return super.processArg(args, optind);
		}
	
	/**
	 * processNode
	 * @param model
	 * @param txn
	 * @param dom
	 * @throws Exception
	 */
	private void processNode(LTModel model,Transaction txn,Node dom)
		throws  Exception
		{
		
		if(dom.getNodeType()==Node.DOCUMENT_NODE)
			{
			Element root=Document.class.cast(dom).getDocumentElement();
			processNode(model,txn,root);
			return;
			}
		
		if(dom.getNodeName().equals("chromosomes"))
			{
			Attr orgId= Element.class.cast(dom).getAttributeNode("organism-id");
			if(orgId==null || !Cast.IntegerGreaterThanZero.isA(orgId.getValue())) throw new IllegalArgumentException(dom.getNodeName()+" : bad or missing @organism-id");
			int organism_id= Integer.parseInt(orgId.getValue());
			if(!model.getOrganismDB().containsKey(txn, organism_id))
				{
				throw new IllegalArgumentException("Organism id "+organism_id+" not defined.");
				}
			for(Node n2=dom.getFirstChild();
				n2!=null; 
				n2=n2.getNextSibling()
				)
				{
				if(n2.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e2=Element.class.cast(n2);
				if(n2.getNodeName().equals("chromosome"))
					{
					processChromosome(model,txn,e2,organism_id);
					}
				else
					{
					throw new IllegalArgumentException("Bad element: "+n2.getNodeName());
					}
				}
			return;
			}
		
		
		LOG.info("Processing "+dom.getNodeName());
		for(Node n1=dom.getFirstChild();
				n1!=null; 
				n1=n1.getNextSibling()
				)
			{
			if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
			Element e=Element.class.cast(n1);
			if(n1.getNodeName().equals("organism"))
				{
				processOrganism(model,txn,e);
				}
			else if(n1.getNodeName().equals("organisms"))
				{
				processNode(model,txn,e);
				}
			else if(n1.getNodeName().equals("track"))
				{
				processTrack(model,txn,e);
				}
			else if(n1.getNodeName().equals("sql-stmt"))
				{
				processSQLInserts(model,txn,e);
				}
			else if(n1.getNodeName().equals("load"))
				{
				processStreamLoader(model,txn,e);
				}
			else if(e.getNodeName().equals("chromosomes"))
				{
				processNode(model,txn,e);
				}
			else
				{
				throw new IllegalArgumentException("Bad element: "+n1.getNodeName()+" under "+dom.getNodeName());
				}
			}
		
		}
	
	/**
	 * 
	 * processChromosome
	 * 
	 */
	private void processChromosome(
			LTModel model,Transaction txn,
			Element root,
			int organism_id
			)
	throws  Exception
		{
		assert(root.getNodeName().equals("chromosome"));
	
		Attr attId= root.getAttributeNode("id");
		if(attId==null || !Cast.IntegerGreaterThanZero.isA(attId.getValue())) throw new IllegalArgumentException(root.getNodeName()+" bad or missing @id");
		int chromosome_id= Integer.parseInt(attId.getValue());
		
		Element node = XML.oneElement(root, "name");
		String name=node.getTextContent();
		if(StringUtils.isEmpty(name)) throw new IllegalArgumentException(root.getNodeName()+" : bad name");
		if(!name.matches("[a-zA-Z]\\w*")) throw new IllegalArgumentException("bad name: "+name);
		
		Object json=null;
		node = XML.firstElement(root,"metadata");
		if(node!=null)
			{
			JSONParser parser= new JSONParser(new StringReader(node.getTextContent()));
			json=parser.any();
			parser.eof();
			}
		ChromInfo chromosome= new ChromInfo(
				chromosome_id,
				organism_id,
				name
				);
		
		chromosome.setObject(json);
		if(model.getChromDB().containsKey(txn, chromosome.getId()))
			{
			LOG.info("duplicate chromosome-id "+chromosome.getId() );
			if(this.replacePolicy==REPLACE_POLICY.IGNORE)
				{
				LOG.info("ignore");
				return;
				}
			else if(this.replacePolicy==REPLACE_POLICY.ERROR)
				{
				throw new IllegalArgumentException(
						"chromosome-id "+chromosome.getId()+" already exists"
						);
				}
			}
		
		ChromInfo ci= model.getChromsomeByName(txn, organism_id, chromosome.getName());
		
		if(ci!=null &&
		   ci.getOrganismId().equals(chromosome.getOrganismId()) &&
		  !ci.getId().equals(chromosome.getId())
		  )
			{
			LOG.info("!=chromosome, same name, same oganism");
			throw new IllegalArgumentException(
				"chromosome-named "+chromosome.getName()+" already exists for this organism "
				+ci.getId()+" vs "+chromosome.getId()
				);
			}
		else
			{
			LOG.info("no other chrom named "+name);
			}
			
		LOG.info("inserting "+chromosome);
		model.getChromDB().put(txn, chromosome.getId(), chromosome);
		}
	
	/**
	 * 
	 * processOrganism
	 * 
	 */
	private void processOrganism(LTModel model,Transaction txn,Element root)
		throws  Exception
		{
		assert(root.getNodeName().equals("organism"));

		Attr attId= root.getAttributeNode("id");
		LOG.info("found organism "+attId);
		if(attId==null || !Cast.IntegerGreaterThanZero.isA(attId.getValue())) throw new IllegalArgumentException(root.getNodeName()+" bad or missing @id");
		int organism_id= Integer.parseInt(attId.getValue());
		
		Element node = XML.oneElement(root, "name");
		String name=node.getTextContent();
		if(StringUtils.isEmpty(name)) throw new IllegalArgumentException(root.getNodeName()+" : bad name");
		if(!name.matches("[a-zA-Z]\\w*")) throw new IllegalArgumentException("bad name: "+name);
		node = XML.firstElement(root,"description");
		String description= null;
		if(node!=null)
			{
			description= node.getTextContent();
			}
		if(StringUtils.isEmpty(description))
			{
			description=name;
			}
		Map<String,Object> json=null;
		node = XML.firstElement(root,"metadata");
		if(node!=null)
			{
			JSONParser parser= new JSONParser(new StringReader(node.getTextContent()));
			json=parser.object();
			parser.eof();
			}
		Organism organism= new Organism(
				organism_id,
				name
				);
		organism.setDescription(description);
		organism.setObject(json);
		if(model.getOrganismDB().containsKey(txn, organism.getId()))
			{
			LOG.info("duplicate organism-id "+organism.getId() );
			if(this.replacePolicy==REPLACE_POLICY.IGNORE)
				{
				LOG.info("ignore");
				return;
				}
			else if(this.replacePolicy==REPLACE_POLICY.ERROR)
				{
				throw new IllegalArgumentException(
						"organism-id "+organism.getId()+" already exists"
						);
				}
			}
		
		Organism other=model.getOrganismByName(txn, organism.getName());
		if(other!=null && !other.getId().equals(organism.getId()))
			{
			throw new IllegalArgumentException(
					"organism named "+organism.getName()+" already exists"
					);
			}
			
		LOG.info("inserting "+organism);
		if(model.getOrganismDB().put(txn, organism.getId(), organism)!=OperationStatus.SUCCESS)
			{
			System.err.println("Cannot insert:"+organism);
			}
		}
	
	/**
	 * processSQLInserts
	 */
	private void processSQLInserts(LTModel model,Transaction txn,Element root)
	throws  Exception
		{
		assert(root.getNodeName().equals("sql-stmt"));
		Attr att= root.getAttributeNode("driver");
		String driver=null;
		if(att!=null) driver="com.mysql.jdbc.Driver"; 
		Class.forName(driver);
		Connection con=null;
		
		att= root.getAttributeNode("uri");
		if(att==null) throw new IllegalArgumentException("missing @jdbc-uri");
		String jdbcUri=att.getValue();
		
		att= root.getAttributeNode("user");
		if(att==null) root.getAttributeNode("login");
		if(att==null) throw new IllegalArgumentException("missing @login");
		String login=att.getValue();
		
		att= root.getAttributeNode("password");
		String password=(att==null?"":att.getValue());
		
		try
			{
			con=DriverManager.getConnection(jdbcUri+"?user="+login+"&password="+password);
			
			for(Node n2=root.getFirstChild();
				n2!=null; 
				n2=n2.getNextSibling()
				)
				{
				if(n2.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e2=Element.class.cast(n2);
				if(n2.getNodeName().equals("sql-insert"))
					{
					processSQLInsert(model,txn,e2,con);
					}
				else
					{
					throw new IllegalArgumentException("Bad element: "+n2.getNodeName());
					}
				}
			}
		catch(Exception err)
			{
			throw err;
			}
		finally
			{
			if(con!=null) con.close();
			}
		}
	
	private void processSQLInsert(LTModel model,Transaction txn,Element root,Connection con)
	throws  Exception
		{
		assert(root.getNodeName().equals("sql-insert"));
		String query= root.getTextContent();
		if(StringUtils.isEmpty(query)) throw new IllegalArgumentException("no query");
		Attr att= root.getAttributeNode("organism-id");
		if(att==null || !Cast.Integer.isA(att.getValue()))
			{
			throw new IllegalArgumentException("bad @organism-id");
			}
		Organism org= model.getOrganismDB().get(txn, Integer.parseInt(att.getValue()));
		if(org==null) throw new IllegalArgumentException("unknown @organism-id:"+att.getValue());
		
		att= root.getAttributeNode("track-id");
		if(att==null || !Cast.Integer.isA(att.getValue()))
			{
			throw new IllegalArgumentException("bad @track-id");
			}
		Track track= model.getTracksDB().get(txn, Integer.parseInt(att.getValue()));
		if(track==null) throw new IllegalArgumentException("unknown @track-id:"+att.getValue());
		
		
		att= root.getAttributeNode("class-loader");
		if(att==null || StringUtils.isEmpty(att.getValue()))
			{
			throw new IllegalArgumentException("bad @class-loader");
			}
		LTSqlLoader loader= (LTSqlLoader)Class.forName(att.getValue()).newInstance();
		
		LOG.info("executeQuery "+query);
		Statement stmt= con.createStatement();
	    ResultSet row=stmt.executeQuery(query); 
	    Map<String, ChromInfo> name2chromInfo=new HashMap<String, ChromInfo>();
		while(loader.next(row))
			{
			MappedObject mapped=loader.getMappedObject();
			ChromInfo ci= name2chromInfo.get(loader.getChromosome());
			if(ci==null)
				{
				ci=model.getChromsomeByName(txn, org.getId(), loader.getChromosome());
				if(ci==null)
					{
					LOG.info("Cannot find chromosome by name"+loader.getChromosome());
					continue;
					}
				name2chromInfo.put(loader.getChromosome(),ci);
				}
			
			long nodeId=model.insert(txn,ci.getId(), mapped, track);
			if(nodeId!=LTNode.NIL)
				{
				for(String s:loader.getKeywords())
					{
					model.getKeyword2nodeId().putNoDupData(txn, s, nodeId);
					}
				}
			}
		row.close();
		stmt.close();
		}
	
	
	private void processStreamLoader(LTModel model,Transaction txn,Element root)
	throws  Exception
		{
		assert(root.getNodeName().equals("load"));
		String uri= root.getTextContent();
		if(StringUtils.isEmpty(uri)) uri=root.getAttribute("uri");
		if(StringUtils.isEmpty(uri)) throw new IllegalArgumentException("no or empty uri");
		uri=uri.trim();
		Attr att= root.getAttributeNode("organism-id");
		if(att==null || !Cast.Integer.isA(att.getValue()))
			{
			throw new IllegalArgumentException("bad @organism-id");
			}
		Organism org= model.getOrganismDB().get(txn, Integer.parseInt(att.getValue()));
		if(org==null) throw new IllegalArgumentException("unknown @organism-id:"+att.getValue());
		
		att= root.getAttributeNode("track-id");
		if(att==null || !Cast.Integer.isA(att.getValue()))
			{
			throw new IllegalArgumentException("bad @track-id");
			}
		Track track= model.getTracksDB().get(txn, Integer.parseInt(att.getValue()));
		if(track==null) throw new IllegalArgumentException("unknown @track-id:"+att.getValue());
		
		
		att= root.getAttributeNode("class-loader");
		if(att==null || StringUtils.isEmpty(att.getValue()))
			{
			throw new IllegalArgumentException("bad @class-loader");
			}
		LTStreamLoader loader= (LTStreamLoader)Class.forName(att.getValue()).newInstance();
		
		Integer limit=null;
		att= root.getAttributeNode("limit");
		if(att!=null && Cast.NonNegativeInteger.isA(att.getValue()))
			{
			limit= new Integer(att.getValue());
			LOG.info("limit to "+limit+" record");
			}
		
		
		
		int nLines=-1;
		LOG.info("executeQuery "+uri);
		Map<String, ChromInfo> name2chromInfo=new HashMap<String, ChromInfo>();
		LOG.info("opening "+uri);
		loader.open(uri);
		LOG.info("opened "+uri);
		while(loader.next())
			{
			++nLines;
			if(limit!=null && nLines>=limit) break;
			ChromInfo ci= name2chromInfo.get(loader.getChromosome());
			if(ci==null)
				{
				ci=model.getChromsomeByName(txn, org.getId(), loader.getChromosome());
				if(ci==null)
					{
					LOG.info("Cannot find chromosome by name"+loader.getChromosome());
					continue;
					}
				name2chromInfo.put(loader.getChromosome(),ci);
				}
			
			long nodeId=model.insert(txn, ci.getId(), loader.getMappedObject(), track);
			if(nodeId!=LTNode.NIL)
				{
				for(String keyword: loader.getKeywords() )
					{
					model.getKeyword2nodeId().putNoDupData(txn, keyword, nodeId);
					}
				}
			}
		loader.close();
		}
	
	
	
	/**
	 * 
	 * processOrganism
	 * 
	 */
	private void processTrack(LTModel model,Transaction txn,Element root)
		throws  Exception
		{
		assert(root.getNodeName().equals("track"));

		Attr attId= root.getAttributeNode("id");
		if(attId==null || !Cast.IntegerGreaterThanZero.isA(attId.getValue())) throw new IllegalArgumentException(root.getNodeName()+" is missing @id");
		int track_id= Integer.parseInt(attId.getValue());
		
		Element node = XML.oneElement(root, "name");
		String name=node.getTextContent();
		if(StringUtils.isEmpty(name)) throw new IllegalArgumentException(root.getNodeName()+" : bad name");
		if(!name.matches("[a-zA-Z]\\w*")) throw new IllegalArgumentException("bad name: "+name);
		
		
		node = XML.firstElement(root,"description");
		String description= null;
		if(node!=null)
			{
			description= node.getTextContent();
			}
		if(StringUtils.isEmpty(description))
			{
			description=name;
			}
		
		Object json=null;
		node = XML.firstElement(root,"metadata");
		if(node!=null)
			{
			JSONParser parser= new JSONParser(new StringReader(node.getTextContent()));
			json=parser.any();
			parser.eof();
			}
		Track track= new Track(
				track_id,
				name
				);
		
		track.setDescription(description);
		track.setObject(json);
		if(model.getTracksDB().containsKey(txn, track.getId()))
			{
			LOG.info("duplicate track-id "+track.getId() );
			if(this.replacePolicy==REPLACE_POLICY.IGNORE)
				{
				LOG.info("ignore");
				return;
				}
			else if(this.replacePolicy==REPLACE_POLICY.ERROR)
				{
				throw new IllegalArgumentException(
						"track-id "+track.getId()+" already exists"
						);
				}
			}
		LOG.info("inserting "+track);
		model.getTracksDB().put(txn, track.getId(), track);
		}
	
	
	private void run(String[] args)
			{
			final long startTime=System.currentTimeMillis();
			int optind=processArgs(args);
			LTModel model=null;
			Transaction txn=null;
			try
				{
				if(getDbHome()==null)
					{
					System.err.println("-db-home not defined");
					return;
					}
				
				DocumentBuilderFactory f=DocumentBuilderFactory.newInstance();
				f.setCoalescing(true);
				f.setNamespaceAware(false);
				f.setValidating(false);
				f.setExpandEntityReferences(true);
				f.setIgnoringComments(false);
				f.setIgnoringElementContentWhitespace(true);
				//f.setSchema(super.loadSchema("/META-INF/schema.xsd"));
				DocumentBuilder docBuilder= f.newDocumentBuilder();
				Document dom;
				
				
				model= new LTModel();
				
				if(Boolean.FALSE.equals(this.transactional))
					{
					LOG.info("setting model non transactional");
					model.setTransactional(false);
					}
				
				model.open(getDbHome(), true);
				
				//EnvironmentMutableConfig mutfg= new EnvironmentMutableConfig();
				//mutfg.setDurability(Durability.COMMIT_NO_SYNC);
				//model.getEnvironment().setMutableConfig(mutfg);
				
				
				if(!Boolean.FALSE.equals(this.transactional))
					{
					txn= model.getEnvironment().beginTransaction(null, null);
					}
				else
					{
					txn=null;
					LOG.info("NO TRANSACTION");
					}
				
				if(optind==args.length)
	                    {
	                    LOG.info("reading from stdin");
	                    dom= docBuilder.parse(System.in);
	                    processNode(model,txn, dom);
	                    }
	            else
	                    {
	                    while(optind< args.length)
	                            {
	                          	String fname=args[optind++];
	                          	LOG.info("reading "+fname);
	                            dom= docBuilder.parse(IOUtils.openInputStream(fname));
	                            processNode(model,txn, dom);
	                            }
	                    }
				if(txn!=null)
					{
					LOG.info("commit");
					txn.commit();
					}
				
				if(LOG.getLevel()!=Level.OFF)
					{
					//EnvironmentStats stats=model.getEnvironment().getStats(null);
					
					
					}
				LOG.info("Done. "+((System.currentTimeMillis()-startTime)/1000.0)+" seconds.");
				}
			catch(Throwable err)
				{
				if(txn!=null) txn.abort();
				err.printStackTrace();
				}
			finally
				{
				if(model!=null)
					{
					
					model.close();
					}
				}
			}
	
		public static void main(String[] args)
			{
			LTLoadComponents org=new LTLoadComponents();
			org.run(args);
			}
	}
